﻿// This code is released under the BSD license.
namespace PugLib.Conditions
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics.CodeAnalysis;
    using System.Linq;
    using System.Linq.Expressions;
    using System.Reflection;

    /// <summary>
    /// Enumerates all the different comparisons which can be performed 
    /// </summary>
    public enum Filter
    {
        None = 0, //ExpressionType.Add == 0, too...
        Or = ExpressionType.Or,
        And = ExpressionType.And,
        Xor = ExpressionType.ExclusiveOr,
        Not = ExpressionType.Not,
        Equal = ExpressionType.Equal,
        NotEqual = ExpressionType.NotEqual,
        OrElse = ExpressionType.OrElse,
        AndAlso = ExpressionType.AndAlso,
        LessThan = ExpressionType.LessThan,
        GreaterThan = ExpressionType.GreaterThan,
        LessThanOrEqual = ExpressionType.LessThanOrEqual,
        GreaterThanOrEqual = ExpressionType.GreaterThanOrEqual,
        // + 1 since we're making stuff up at this point to handle custom functions
        Contains = ExpressionType.TypeIs + 1, // TypeIs is the highest valued ExpressionType
        StartsWith = Contains + 1,
        Is = StartsWith + 1,
        IsNot = Is + 1,
        DoesNotContain = IsNot + 1,
        EndsWith = DoesNotContain + 1,
        WithinThePastDays = EndsWith + 1,
    }

    public abstract class Condition
    {
        #region "Fields"

        //Used to ensure we get the same instance of a particular ParameterExpression 
        //across multiple queries 
        private static readonly Dictionary<string, ParameterExpression> ParamTable =
            new Dictionary<string, ParameterExpression>();

        //The expression tree which will be passed to the LINQ to SQL runtime 

        public LambdaExpression LambdaExpr { get; set; }

        #endregion

        #region "Public Methods"



        public static Condition<T, TValue> Create<T, TValue>(T dataSource, string propertyName, Filter condType, TValue value)
        {
            return new Condition<T, TValue>(dataSource, propertyName, condType, value);
        }

        /// <summary> 
        /// Creates a Condition which combines two other Conditions 
        /// </summary> 
        /// <typeparam name="T">The type the condition will execute against</typeparam> 
        /// <param name="cond1">The first Condition</param> 
        /// <param name="condType">The operator to use on the conditions</param> 
        /// <param name="cond2">The second Condition</param> 
        /// <returns>A new Condition which combines two Conditions into one according to the specified operator</returns> 
        /// <remarks></remarks> 
        public static Condition<T> Combine<T>(Condition<T> cond1, Filter condType, Condition<T> cond2)
        {
            return Condition<T>.Combine(cond1, condType, cond2);
        }

        /// <summary>
        /// Combines multiple conditions according to the specified operator 
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="cond1"></param>
        /// <param name="condType"></param>
        /// <param name="conditions"></param>
        /// <returns></returns>
        public static Condition<T> Combine<T>(Condition<T> cond1, Filter condType, params Condition<T>[] conditions)
        {
            return Condition<T>.Combine(cond1, condType, conditions);
        }

        #endregion

        #region "Protected Methods"

        /// <summary>
        /// Combines two Expressions according to the specified operator (condType) 
        /// </summary>
        /// <param name="left"></param>
        /// <param name="condType"></param>
        /// <param name="right"></param>
        /// <returns></returns>
        [SuppressMessage("Microsoft.Maintainability", "CA1502:AvoidExcessiveComplexity")]
        protected static Expression CombineExpression(Expression left, Filter condType, Expression right)
        {
            try
            {
                //Join the Expressions based on the operator 
                switch (condType)
                {
                    #region unsupported/untested defaults

                    case Filter.Or:
                        return Expression.Or(left, right);
                    case Filter.And:
                        return Expression.And(left, right);
                    case Filter.Xor:
                        return Expression.ExclusiveOr(left, right);
                    case Filter.Equal:
                        return Expression.Equal(left, right);
                    case Filter.OrElse:
                        return Expression.OrElse(left, right);
                    case Filter.AndAlso:
                        return Expression.AndAlso(left, right);
                    case Filter.NotEqual:
                        return Expression.NotEqual(left, right);
                    case Filter.LessThanOrEqual:
                        return Expression.LessThanOrEqual(left, right);
                    case Filter.GreaterThanOrEqual:
                        return Expression.GreaterThanOrEqual(left, right);

                    #endregion unsupported/untested defaults

                    case Filter.Contains:
                        return Expression.Call(GetMethod("Contains", left, right), left, right);
                    case Filter.DoesNotContain:
                        return Expression.Call(GetMethod("DoesNotContain", left, right), left, right);
                    case Filter.StartsWith:
                        return Expression.Call(GetMethod("StartsWith", left, right), left, right);
                    case Filter.EndsWith:
                        return Expression.Call(GetMethod("EndsWith", left, right), left, right);
                    case Filter.Is:
                        return Expression.Call(GetMethod("Is", left, right), left, right);
                    case Filter.IsNot:
                        return Expression.Call(GetMethod("IsNot", left, right), left, right);
                    case Filter.LessThan:
                        return Expression.Call(GetMethod("LessThan", left, right), left, right);
                    case Filter.GreaterThan:
                        return Expression.Call(GetMethod("GreaterThan", left, right), left, right);
                    case Filter.WithinThePastDays:
                        return Expression.Call(GetMethod("WithinThePastDays", left, right), left, right);
                    default:
                        throw new ArgumentException("Not a valid Conditions Type", "condType", null);
                }
            }
            catch (InvalidOperationException)
            {
                return null;
            }
        }

        protected static Func<T, bool> CombineFunc<T>(Func<T, bool> d1, Filter condType, Func<T, bool> d2)
        {
            switch (condType)
            {
                case Filter.Or:
                    return x => d1(x) | d2(x);
                case Filter.And:
                    return x => d1(x) & d2(x);
                case Filter.Xor:
                    return x => d1(x) ^ d2(x);
                case Filter.Equal:
                    return x => d1(x) == d2(x);
                case Filter.OrElse:
                    return x => d1(x) || d2(x);
                case Filter.AndAlso:
                    return x => d1(x) && d2(x);
                case Filter.NotEqual:
                    return x => d1(x) != d2(x);
                //case Compare.LessThan:
                //    return x => d1(x) < d2(x);
                //case Compare.GreaterThan:
                //    return x => d1(x) > d2(x);
                //case Compare.LessThanOrEqual:
                //    return x => d1(x) <= d2(x);
                //case Compare.GreaterThanOrEqual:
                //    return x => d1(x) >= d2(x);
                default:
                    throw new ArgumentException("Not a valid Conditions Type", "condType");
            }
        }

        //Guarantees that we get the same instance of a ParameterExpression for a given type t. 
        protected static ParameterExpression GetParamInstance(Type dataType)
        {
            //Parameters are matched by reference, not by name, so we cache the instances in a Dictionary. 
            if (!ParamTable.ContainsKey(dataType.Name))
            {
                ParamTable.Add(dataType.Name, Expression.Parameter(dataType, dataType.Name));
            }
            return ParamTable[dataType.Name];
        }

        #region method wrappers

        private static MethodInfo GetMethod(string methodName, Expression left, Expression right)
        {
            return typeof(SearchMethods).GetMethod(methodName, new[] { left.Type, right.Type });
        }

        #endregion method wrappers

        #endregion
    }

    public static class ConditionExtensions
    {
        /// <summary>
        /// Filters an IQueryable(Of T) according to the specified condition 	
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="source"></param>
        /// <param name="condition"></param>
        /// <returns></returns>
        public static IQueryable<T> Where<T>(this IQueryable<T> source, Condition condition)
        {
            MethodCallExpression callExpr = Expression.Call(typeof(Queryable), "Where", new[] { source.ElementType },
                                                            source.Expression, Expression.Quote(condition.LambdaExpr));
            return (IQueryable<T>)source.Provider.CreateQuery(callExpr);
        }

        /// <summary>
        /// Filters an IEnumerable(Of T) according to the specified condition 		
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="source"></param>
        /// <param name="condition"></param>
        /// <returns></returns>
        public static IEnumerable<T> Where<T>(this IEnumerable<T> source, Condition<T> condition)
        {
            return source.Where(condition.Del);
        }

        public static Condition<T, TValue> CreateCondition<T, TValue>(this T dataSource, string propName, Filter condType, TValue value)
        {
            return Condition.Create(dataSource, propName, condType, value);
        }
    }
}